// Copyright (c) Appeon. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 
// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.

using System;
using System.Collections.Generic;
using System.Data;
using SnapObjects.Commons;
using SnapObjects.Data.Internal;


namespace SnapObjects.Data.MySql
{
    public class MySqlTypeMapper : TypeMapper
    {

        private readonly IntTypeMapping _int = new IntTypeMapping("int", DbType.Int32);

        private readonly UIntTypeMapping _uint = new UIntTypeMapping("int unsigned", DbType.UInt32);

        private readonly LongTypeMapping _bigint = new LongTypeMapping("bigint", DbType.Int64);

        private readonly ULongTypeMapping _ubigint = new ULongTypeMapping("bigint unsigned", DbType.UInt64);

        private readonly ShortTypeMapping _smallint = new ShortTypeMapping("smallint", DbType.Int16);

        private readonly UShortTypeMapping _usmallint = new UShortTypeMapping("smallint unsigned", DbType.UInt16);

        private readonly ByteTypeMapping _utinyint = new ByteTypeMapping("tinyint unsigned", DbType.Byte);

        private readonly SByteTypeMapping _sbyte = new SByteTypeMapping("tinyint", DbType.SByte);

        private readonly BoolTypeMapping _bool = new BoolTypeMapping("bit", DbType.Boolean);

        private readonly DecimalTypeMapping _decimal = new DecimalTypeMapping("decimal(18, 2)", DbType.Decimal);

        private readonly DoubleTypeMapping _double = new DoubleTypeMapping("double");

        private readonly FloatTypeMapping _float = new FloatTypeMapping("float");

        private readonly FloatTypeMapping _real = new MySqlFloatTypeMapping("real", DbType.Single);

        private readonly MySqlByteArrayTypeMapping _binary = new MySqlByteArrayTypeMapping("binary", DbType.Binary, size: 255);

        private readonly MySqlByteArrayTypeMapping _varbinary = new MySqlByteArrayTypeMapping("varbinary", DbType.Binary);

        private readonly MySqlByteArrayTypeMapping _unboundedBinary = new MySqlByteArrayTypeMapping("varbinary");

        private readonly MySqlByteArrayTypeMapping _keyBinary = new MySqlByteArrayTypeMapping("varbinary", dbType: DbType.Binary, size: 900);

        private readonly MySqlCharTypeMapping _char = new MySqlCharTypeMapping("char", dbType: DbType.AnsiStringFixedLength);

        private readonly StringTypeMapping _varchar = new MySqlStringTypeMapping("varchar", DbType.AnsiString, size: 255);

        private readonly StringTypeMapping _longText = new MySqlStringTypeMapping("longtext", DbType.String);

        private readonly StringTypeMapping _mediumText = new MySqlStringTypeMapping("mediumtext", DbType.String);

        private readonly StringTypeMapping _text = new MySqlStringTypeMapping("text", DbType.String);

        private readonly StringTypeMapping _tinyText = new MySqlStringTypeMapping("tinytext", DbType.String);

        private readonly MySqlDateTimeTypeMapping _datetime = new MySqlDateTimeTypeMapping("datetime", dbType: DbType.DateTime);

        private readonly MySqlDateTimeOffsetTypeMapping _datetimeoffset = new MySqlDateTimeOffsetTypeMapping("datetimeoffset");

        private readonly TimeSpanTypeMapping _time = new MySqlTimeSpanTypeMapping("time", DbType.Time);

        private readonly MySqlGuidTypeMapping _guid = new MySqlGuidTypeMapping("CHAR(36)", DbType.Guid, size: 36);

        private readonly MySqlStringTypeMapping _unboundedUnicodeString
            = new MySqlStringTypeMapping("varchar", dbType: null, unicode: true);

        private readonly MySqlStringTypeMapping _keyUnicodeString
            = new MySqlStringTypeMapping("varchar", dbType: null, unicode: true, size: 255);

        private readonly MySqlStringTypeMapping _unboundedAnsiString
            = new MySqlStringTypeMapping("varchar", dbType: DbType.AnsiString);

        private readonly MySqlStringTypeMapping _keyAnsiString
            = new MySqlStringTypeMapping("varchar", dbType: DbType.AnsiString, unicode: false, size: 255);

        private readonly MySqlStringTypeMapping _fixedLengthAnsiString
            = new MySqlStringTypeMapping("char", dbType: DbType.AnsiString);


        private readonly Dictionary<string, TypeMapping> _dbTypeMappings;

        private readonly Dictionary<Type, TypeMapping> _clrTypeMappings;

        private readonly HashSet<string> _disallowedMappings;

        public MySqlTypeMapper(IDataContextOptions options) : base(options)
        {
            _dbTypeMappings
                = new Dictionary<string, TypeMapping>(StringComparer.OrdinalIgnoreCase)
                {
                    { "bigint", _bigint },
                    { "bigint unsigned", _ubigint },
                    { "decimal", _decimal },
                    { "float", _float },
                    { "double", _double },
                    { "int", _int },
                    { "int unsigned", _uint },
                    { "mediumint", _int },
                    { "mediumint unsigned", _uint },
                    { "real", _real },
                    { "smallint", _smallint },
                    { "smallint unsigned", _usmallint },
                    { "tinyint", _bool },
                    { "tinyint unsigned", _utinyint },
                    { "char", _fixedLengthAnsiString },
                    { "varchar",_varchar },
                    { "longtext", _longText},
                    { "mediumtext", _mediumText},
                    { "text", _text},
                    { "tinytext", _tinyText},
                    { "datetime", _datetime },
                    { "timestamp", _datetime },
                    { "bit", _bool },
                    { "binary", _binary },
                    { "varbinary", _varbinary },
                    { "tinyblob", _varbinary },
                    { "blob", _varbinary },
                    { "mediumblob", _varbinary },
                    { "longblob", _varbinary },
                };

            // Note: sbyte, ushort, uint, char and ulong type mappings are not supported by SQL Server.
            // We would need the type conversions feature to allow this to work - see https://github.com/aspnet/EntityFramework/issues/242.
            _clrTypeMappings
                = new Dictionary<Type, TypeMapping>
                {
                    { typeof(int), _int },
                    { typeof(long), _bigint },
                    { typeof(DateTime), _datetime },
                    { typeof(DateTimeOffset), _datetimeoffset },
                    { typeof(TimeSpan), _time },
                    { typeof(bool),  _bool },
                    { typeof(byte),  _utinyint },
                    { typeof(double), _double },
                    { typeof(char), _char },
                    { typeof(Guid), _guid },
                    { typeof(short), _smallint },
                    { typeof(float), _float },
                    { typeof(decimal), _decimal },
                    { typeof(byte[]), _varbinary },
                    { typeof(sbyte),_sbyte },
                    { typeof(ushort), _usmallint},
                    { typeof(uint), _uint},
                    { typeof(ulong), _ubigint }
                };

            // These are disallowed only if specified without any kind of length specified in parenthesis.
            // This is because we don't try to make a new type from this string and any max length value
            // specified in the model, which means use of these strings is almost certainly an error, and
            // if it is not an error, then using, for example, varbinary(1) will work instead.
            _disallowedMappings
                = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
                {
                    "binary varying",
                    "char varying",
                    "character varying",
                    "character",
                    "national char varying",
                    "national character varying",
                    "national character",
                    "nchar",
                    "nvarchar"
                };

            this.ByteArrayMapper
                = new ByteArrayTypeMapper(
                    maxBoundedLength: 8000,
                    defaultMapping: _unboundedBinary,
                    unboundedMapping: _unboundedBinary,
                    keyMapping: _keyBinary,
                    createBoundedMapping: size => new MySqlByteArrayTypeMapping(
                        "varbinary(" + size + ")",
                        DbType.Binary,
                        size));

            this.StringMapper
                = new StringTypeMapper(
                    maxBoundedAnsiLength: 8000,
                    defaultAnsiMapping: _unboundedAnsiString,
                    unboundedAnsiMapping: _unboundedAnsiString,
                    keyAnsiMapping: _keyAnsiString,
                    createBoundedAnsiMapping: size => new MySqlStringTypeMapping(
                        "varchar(" + size + ")",
                        DbType.AnsiString,
                        unicode: false,
                        size: size),
                    maxBoundedUnicodeLength: 4000,
                    defaultUnicodeMapping: _unboundedUnicodeString,
                    unboundedUnicodeMapping: _unboundedUnicodeString,
                    keyUnicodeMapping: _keyUnicodeString,
                    createBoundedUnicodeMapping: size => new MySqlStringTypeMapping(
                        "varchar(" + size + ")",
                        dbType: null,
                        unicode: true,
                        size: size));
        }

        public override IByteArrayTypeMapper ByteArrayMapper { get; }

        public override IStringTypeMapper StringMapper { get; }

        public override void ValidateTypeName(string dbTypeName)
        {
            if (_disallowedMappings.Contains(dbTypeName))
            {
                throw new ArgumentException(MySqlStrings.UnqualifiedDataType(dbTypeName));
            }
        }

        protected override IReadOnlyDictionary<Type, TypeMapping> GetClrTypeMappings()
        {
            return _clrTypeMappings;
        }

        protected override IReadOnlyDictionary<string, TypeMapping> GetDbTypeMappings()
        {
            return _dbTypeMappings;
        }

        public override TypeMapping FindMapping(Type clrType)
        {
            Check.NotNull(clrType, nameof(clrType));

            clrType = TypeHelper.UnwrapEnumType(TypeHelper.UnwrapNullableType(clrType));

            return clrType == typeof(string)
                ? _unboundedUnicodeString
                : (clrType == typeof(byte[])
                    ? _unboundedBinary
                    : base.FindMapping(clrType));
        }
    }
}
